package com.mhy.dexload;

import android.content.Context;
import android.util.Log;
import android.widget.Toast;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

import static com.mhy.dexload.DexUtils.APP_DEX;


/**
 * Created By Mahongyin
 * Date    2021/2/27 16:39
 */
public class Hotfix {
    /**
     *
     * @param context
     * @param patchDexFile
     * @param patchClassName  需要修复的类  包类
     * @throws ClassNotFoundException
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     */
    public static void patch(Context context, String patchDexFile, String patchClassName) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //获取系统PathClassLoader的"dexElements"属性值
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        Object origDexElements = getDexElements(pathClassLoader);

        //新建DexClassLoader并获取“dexElements”属性值
        String otpDir = context.getDir(APP_DEX, Context.MODE_PRIVATE).getAbsolutePath();
        Log.i("hotfix", "otpdir=" + otpDir);
        DexClassLoader nDexClassLoader = new DexClassLoader(patchDexFile, otpDir, patchDexFile, context.getClassLoader());
        Object patchDexElements = getDexElements(nDexClassLoader);

        //将patchDexElements插入原origDexElements前面
        Object allDexElements = combineArray(origDexElements, patchDexElements);

        //将新的allDexElements重新设置回pathClassLoader
        setDexElements(pathClassLoader, allDexElements);

        //重新加载类
        pathClassLoader.loadClass(patchClassName);
    }

    private static Object getDexElements(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        //首先获取ClassLoader的“pathList”实例
        Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
        pathListField.setAccessible(true);//设置为可访问
        Object pathList = pathListField.get(classLoader);

        //然后获取“pathList”实例的“dexElements”属性
        Field dexElementField = pathList.getClass().getDeclaredField("dexElements");
        dexElementField.setAccessible(true);

        //读取"dexElements"的值
        Object elements = dexElementField.get(pathList);
        return elements;
    }

    //合拼dexElements
    private static Object combineArray(Object obj, Object obj2) {
        Class componentType = obj2.getClass().getComponentType();
        //读取obj长度
        int length = Array.getLength(obj);
        //读取obj2长度
        int length2 = Array.getLength(obj2);
        Log.i("hotfix", "length=" + length + ",length2=" + length2);
        //创建一个新Array实例，长度为ojb和obj2之和
        Object newInstance = Array.newInstance(componentType, length + length2);
        for (int i = 0; i < length + length2; i++) {
            //把obj2元素插入前面
            if (i < length2) {
                Array.set(newInstance, i, Array.get(obj2, i));
            } else {
                //把obj元素依次放在后面
                Array.set(newInstance, i, Array.get(obj, i - length2));
            }
        }
        //返回新的Array实例
        return newInstance;
    }

    private static void setDexElements(ClassLoader classLoader, Object dexElements) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        //首先获取ClassLoader的“pathList”实例
        Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
        pathListField.setAccessible(true);//设置为可访问
        Object pathList = pathListField.get(classLoader);

        //然后获取“pathList”实例的“dexElements”属性
        Field declaredField = pathList.getClass().getDeclaredField("dexElements");
        declaredField.setAccessible(true);
        //设置"dexElements"的值
        declaredField.set(pathList, dexElements);
    }




    private static HashSet<File> loadedDex = new HashSet<File>();

    static {
        loadedDex.clear();
    }

    public void hotFix(Context mContext,String filePath) {
        //移置dex文件位置（从sd卡中移置到应用目录）
        InputStream is = null;
        FileOutputStream os = null;
        File dexOutputDir = mContext.getDir(APP_DEX, Context.MODE_PRIVATE);
        try {
            is = new FileInputStream(filePath);
            os = new FileOutputStream(dexOutputDir);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }

            //测试是否成功写入
            File fileTest = new File(filePath);
            if (fileTest.exists()) {
                Toast.makeText(mContext, "dex重写成功", Toast.LENGTH_SHORT).show();
            }
            //获取到已修复的dex文件，进行热修复操作
            File[] listFiles = dexOutputDir.listFiles();
            for (File file : listFiles) {
                if (file.getName().startsWith("classes") && file.getName().endsWith("dex")) {
                    loadedDex.add(file);
                }
            }
            //合并之前到dex
            doDexInject(mContext, dexOutputDir);
        } catch (Exception e) {
        }
    }

    /**
     * 通过PathClassLoader、DexClassLoader合并dex文件，实现类替换修复
     *
     * @param context  上下文环境
     * @param filesDir dex所在的文件目录
     */
    private static void doDexInject(Context context, File filesDir) {
        //dex文件需要被写入的目录
        String optimizeDir = filesDir.getAbsolutePath() + File.separator + "opt_dex";
        File fileOpt = new File(optimizeDir);
        if (!fileOpt.exists()) {
            fileOpt.mkdirs();
        }

        //1.获得加载应用程序dex的PathClassLoader
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

        for (File dex : loadedDex) {
            //2.获得加载指定路径下dex的DexClassLoader
            DexClassLoader dexClassLoader = new DexClassLoader(
                    dex.getAbsolutePath(),
                    fileOpt.getAbsolutePath(),
                    null,
                    pathClassLoader);
            //3.合并dex
            try {
                Object dexObj = getPathList(dexClassLoader);
                Object pathObj = getPathList(pathClassLoader);
                Object fixDexElements = getDexElements(dexObj);
                Object pathDexElements = getDexElements(pathObj);
                //合并两个数组
                Object newDexElements = combineArray2(fixDexElements, pathDexElements);
                //重新赋值给PathClassLoader 中的exElements数组
                Object pathList = getPathList(pathClassLoader);
                setField(pathList, pathList.getClass(), "dexElements", newDexElements);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static Object getPathList(Object baseDexClassLoader) throws Exception {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    private static Object getDexElements(Object obj) throws Exception {
        return getField(obj, obj.getClass(), "dexElements");
    }

    /**
     * 通过反射获得对应类
     *
     * @param obj   Object类对象
     * @param cl    class对象
     * @param field 获得类的字符串名称
     * @return
     */
    private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    /**
     * 通过反射修改值
     *
     * @param obj   待修改值
     * @param cl    class对象
     * @param field 待修改值的字符串名称
     * @param value 修改值
     */
    private static void setField(Object obj, Class<?> cl, String field, Object value) throws Exception {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
     * 两个数组合并
     *
     * @param arrayLhs
     * @param arrayRhs
     * @return
     */
    private static Object combineArray2(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }

}
